{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## FullControl AnyAngle Phone Stand\n",
    "\n",
    "this model is a more detailed version of that available on the [fullcontrol website ](https://fullcontrol.xyz/#/models/4d0e78)\n",
    "\n",
    "*<<< check out other demo models [here](https://github.com/FullControlXYZ/fullcontrol/tree/master/models/README.md) >>>*\n",
    "  \n",
    "press ctrl+F9 to run all cells in this notebook, or press shift+enter to run each cell sequentially \n",
    "\n",
    "if you change one of the code cells, make sure you run it and all subsequent cells again (in order)\n",
    "\n",
    "*this document is a jupyter notebook - if they're new to you, check out how they work: [link](https://www.google.com/search?q=ipynb+tutorial), [link](https://jupyter.org/try-jupyter/retro/notebooks/?path=notebooks/Intro.ipynb), [link](https://colab.research.google.com/)*\n### be patient :)\n\nthe next code cell may take a while because running it causes several things to happen:\n- connect to a google colab server -> download the fullcontrol code -> install the fullcontrol code\n\ncheck out [other tutorials](https://github.com/FullControlXYZ/fullcontrol/blob/master/tutorials/README.md) to understand the python code for the FullControl design"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "if 'google.colab' in str(get_ipython()):\n  !pip install git+https://github.com/FullControlXYZ/fullcontrol --quiet\nimport fullcontrol as fc\nfrom google.colab import files\n",
    "from math import cos, sin, tau\n",
    "from copy import deepcopy"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# printer/gcode parameters\n",
    "\n",
    "design_name = 'anyangle_phone_stand'\n",
    "nozzle_temp = 210\n",
    "bed_temp = 40\n",
    "material_flow_percent = 100\n",
    "print_speed_percent = 100\n",
    "fan_percent = 100\n",
    "printer_name='prusa_i3' # generic / ultimaker2plus / prusa_i3 / ender_3 / cr_10 / bambulab_x1 / toolchanger_T0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "# design parameters\n",
    "\n",
    "phone_width = 75\n",
    "# Phone Width (mm) - Measure with the case on if using one\n",
    "# default value: 75 ; guideline range: 10 to 200\n",
    "\n",
    "phone_thickness = 9\n",
    "# Phone Thickness (mm) - Measure with the case on if using one\n",
    "# default value: 9 ; guideline range: 4 to 20\n",
    "\n",
    "height = 30\n",
    "# Stand Height (mm) -\n",
    "# default value: 30 ; guideline range: 20 to 40\n",
    "\n",
    "angles = 13\n",
    "# Stand Angles (mm) - The number of angles the stand can orient the phone in landscape mode\n",
    "# default value: 13 ; guideline range: 9 to 19\n",
    "\n",
    "clamping_tightness_percent = 50\n",
    "# Clamping Tightness (%) - Increse this parameter to reduce the size of the stand and increase clamping forces on the phone\n",
    "# default value: 50 ; guideline range: 0 to 100\n",
    "\n",
    "wave_size_percent = 100\n",
    "# Wave Size (%) - Control how much the wave shapes of the stand protrude\n",
    "# default value: 100 ; guideline range: 50 to 150\n",
    "\n",
    "angry = False\n",
    "# Angry Mode - Make the phone stand look angry. Phone slots are not added in angry mode - set Stand Angles = 11 to 15 for a snug fit between the final two angry spikes\n",
    "# default value: False"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "target = 'visualize'  # 'visualize' or 'gcode'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "# generate the design (make sure you've run the above cells before running this cell)\n",
    "\n",
    "# 0.3 # try 0.6, 0.3 or 1, 0.5 (slow speeds down proportionally for bigger size)\n",
    "EW, EH = 0.8, 0.4\n",
    "\n",
    "if target == \"visualize\":\n",
    "    EH = 3*EH\n",
    "layers = int(height/EH)\n",
    "overlap = EW*0.25  # overlap between extrusions when the waves touch tip to tip\n",
    "initial_print_speed = 500  # print speed for the first layer\n",
    "# number of layers (after the first layer) during which speed increases\n",
    "layers_for_speed_increase = 2\n",
    "# print speed increases to this gradually over layers 2 & 3 (if layers_for_speed_increase=2)\n",
    "main_print_speed = 2*initial_print_speed\n",
    "\n",
    "\n",
    "amp_in_out = 3.25  # 3 # mm # 2.5?\n",
    "amp_mid = 2.25  # 2 # mm # 2.5?\n",
    "if angles % 2 < 0.000001:\n",
    "    print('use an odd number of angles\\n'*20)\n",
    "\n",
    "# centre point for the medial axis of the part\n",
    "centre = fc.Point(x=0, y=0, z=0)\n",
    "# -1 because there are two extra half-angles protruding to the front of the phone (extra_period_outer_inner)\n",
    "freq = (angles-1)*2\n",
    "angle_offset_outer_inner, angle_offset_mid = tau/2, tau/4  # phase shift for wave\n",
    "# how much of an extra sin period to print past 90 degres\n",
    "extra_period_outer_inner = 0.5\n",
    "extra_angle_outer_inner = (tau/((angles-1)*2))*extra_period_outer_inner\n",
    "# this is how much narrower the part is in the middle layer compared to top/bottom\n",
    "mid_contraction_fraction = 0.5\n",
    "# 0.1 #options 0.05, 0.075, 0.125# fraction by which the model's arc-radius is reduced and arc-length extended, to clamp the phone\n",
    "all_contraction_fraction = 0.05\n",
    "\n",
    "amp_in_out = amp_in_out*wave_size_percent/100\n",
    "all_contraction_fraction = all_contraction_fraction*clamping_tightness_percent/100\n",
    "\n",
    "segs = angles*8  # 8#4#8#16 # segments per half-circle plus an angle\n",
    "outer_wave_shape_factor, mid_wave_shape_factor, inner_wave_shape_factor = 2, 1.75, 1.5\n",
    "# 8,8,8#4,4,4#1.5,1,1 # 2,2,2#5,5,5 #3,1,2 #1.5,1,1...(GOOD FOR 13 ANGLES:2,1.5,1.5)\n",
    "\n",
    "# 8#4#16#32 # segments for each of the semicircles at the ends of the part\n",
    "segs_semicircles = 16\n",
    "\n",
    "if angry:\n",
    "  outer_wave_shape_factor, mid_wave_shape_factor, inner_wave_shape_factor = 8, 8, 8\n",
    "  segs_semicircles = 2\n",
    "\n",
    "# remove 1 mm from measure phone thickness due to typical rounded edges\n",
    "phone_thickness -= 1\n",
    "# this factor naturally increases the side of the slot, so it must be reduced now to compensate\n",
    "phone_thickness = phone_thickness*(1-all_contraction_fraction)\n",
    "\n",
    "# next line is means the sides of the phone protrude to the depth of the first fluctating sine wave (in2) as opposed to the medial axis\n",
    "medial_rad = phone_width/2 + amp_mid + EW\n",
    "rad_in2 = medial_rad - ((EW-overlap)/2) - amp_mid - (EW-overlap)\n",
    "rad_in1 = medial_rad - ((EW-overlap)/2) - amp_mid\n",
    "rad_out1 = medial_rad + ((EW-overlap)/2) + amp_mid\n",
    "rad_out2 = medial_rad + ((EW-overlap)/2) + amp_mid + (EW-overlap)\n",
    "\n",
    "centre_semicircle_top2 = fc.polar_to_point(\n",
    "    centre, medial_rad, tau/4 - extra_angle_outer_inner)\n",
    "centre_semicircle_top1 = fc.polar_to_point(\n",
    "    centre, medial_rad, tau/4 - extra_angle_outer_inner)\n",
    "centre_semicircle_bottom2 = fc.polar_to_point(\n",
    "    centre, medial_rad, 3*tau/4 + extra_angle_outer_inner)\n",
    "centre_semicircle_bottom1 = fc.polar_to_point(\n",
    "    centre, medial_rad, 3*tau/4 + extra_angle_outer_inner)\n",
    "\n",
    "# in2 arc to top\n",
    "mydesign = fc.arcXY(centre, rad_in2, tau/2, -\n",
    "                    (tau/4+extra_angle_outer_inner), int(segs/2))\n",
    "line_id_list = ['in2']*(int(segs/2)+1)\n",
    "# top big semicircle\n",
    "mydesign += fc.arcXY(centre_semicircle_top2, medial_rad-rad_in2+amp_in_out,\n",
    "                     3*tau/4 - extra_angle_outer_inner, tau/2, segs_semicircles)\n",
    "line_id_list += ['semicircle_top2']*(segs_semicircles+1)\n",
    "# out2 arc\n",
    "mydesign += fc.arcXY(centre, rad_out2, tau/4-extra_angle_outer_inner,\n",
    "                     tau/2+extra_angle_outer_inner*2, segs)\n",
    "line_id_list += ['out2']*(segs+1)\n",
    "# bottom big semicircle\n",
    "mydesign += fc.arcXY(centre_semicircle_bottom2, medial_rad-rad_in2 +\n",
    "                     amp_in_out, 3*tau/4 + extra_angle_outer_inner, tau/2, segs_semicircles)\n",
    "line_id_list += ['semicircle_bottom2']*(segs_semicircles+1)\n",
    "# in2 arc to middle\n",
    "mydesign += fc.arcXY(centre, rad_in2, 3*tau/4+extra_angle_outer_inner, -\n",
    "                     (tau/4+extra_angle_outer_inner), int(segs/2))\n",
    "line_id_list += ['in2']*(int(segs/2)+1)\n",
    "\n",
    "# in1 arc to top\n",
    "mydesign += fc.arcXY(centre, rad_in1, tau/2, -\n",
    "                     (tau/4+extra_angle_outer_inner), int(segs/2))\n",
    "line_id_list += ['in1']*(int(segs/2)+1)\n",
    "# top semicircle 1\n",
    "mydesign += fc.arcXY(centre_semicircle_top1, medial_rad-rad_in1,\n",
    "                     3*tau/4 - extra_angle_outer_inner, tau/2, segs_semicircles)\n",
    "line_id_list += ['semicircle_top1']*(segs_semicircles+1)\n",
    "# out1 arc\n",
    "mydesign += fc.arcXY(centre, rad_out1, tau/4-extra_angle_outer_inner,\n",
    "                     tau/2+extra_angle_outer_inner*2, segs)\n",
    "line_id_list += ['out1']*(segs+1)\n",
    "# bottom semicircle 1\n",
    "mydesign += fc.arcXY(centre_semicircle_bottom1, medial_rad-rad_in1,\n",
    "                     3*tau/4 + extra_angle_outer_inner, tau/2, segs_semicircles)\n",
    "line_id_list += ['semicircle_bottom1']*(segs_semicircles+1)\n",
    "# in1 arc to middle\n",
    "mydesign += fc.arcXY(centre, rad_in1, 3*tau/4+extra_angle_outer_inner, -\n",
    "                     (tau/4+extra_angle_outer_inner), int(segs/2))\n",
    "line_id_list += ['in1']*(int(segs/2)+1)\n",
    "\n",
    "points_per_layer = len(mydesign)\n",
    "\n",
    "for i in range(len(mydesign)):\n",
    "  # calculate parameters\n",
    "  radius_offset = 0\n",
    "  angle_now = fc.point_to_polar(mydesign[i], centre).angle\n",
    "  if angle_now < -0.00001:\n",
    "      angle_now += tau\n",
    "  line_id = line_id_list[i]\n",
    "  # calculate radial offset\n",
    "  if line_id == 'out2':\n",
    "    radius_offset = amp_in_out * \\\n",
    "        (0.5+0.5*cos(angle_now*freq-angle_offset_outer_inner))**outer_wave_shape_factor\n",
    "  elif line_id == 'in2':\n",
    "    radius_offset = -amp_in_out * \\\n",
    "        (0.5+0.5*cos(angle_now*freq-angle_offset_outer_inner))**inner_wave_shape_factor\n",
    "  if mydesign[i].x < 0:  # don't add a wave for the last bit of the middle lines so they can have a semicircle join their ends\n",
    "    if line_id == 'out1':\n",
    "      radius_offset = -amp_mid * \\\n",
    "          (0.5+0.5*cos(angle_now*freq-angle_offset_outer_inner))**mid_wave_shape_factor\n",
    "    elif line_id == 'in1':\n",
    "      radius_offset = amp_mid * \\\n",
    "          (0.5+0.5*cos(angle_now*freq-angle_offset_outer_inner))**mid_wave_shape_factor\n",
    "  else:  # do a double reduced-period reduced-amplitude wave for the last one on inner section to end it within the outer loop\n",
    "    if line_id == 'out1':\n",
    "      radius_offset = -amp_mid * \\\n",
    "          (0.5+0.5*cos(2*angle_now*freq-angle_offset_outer_inner))**mid_wave_shape_factor\n",
    "    elif line_id == 'in1':\n",
    "      radius_offset = amp_mid * \\\n",
    "          (0.5+0.5*cos(2*angle_now*freq-angle_offset_outer_inner))**mid_wave_shape_factor\n",
    "  # offset path as required\n",
    "  mydesign[i] = fc.move_polar(mydesign[i], centre, radius_offset, 0)\n",
    "\n",
    "# slots\n",
    "if not angry:  # slots do not work well in angry mode\n",
    "    for i in range(len(mydesign)):\n",
    "        if line_id_list[i] == 'in2' or line_id_list[i] == 'semicircle_top2' or line_id_list[i] == 'semicircle_bottom2':\n",
    "            slot_half_width = phone_thickness/2\n",
    "            slot_half_height = phone_width/2+EW/2\n",
    "            if abs(mydesign[i].x) <= slot_half_width and abs(mydesign[i].y) <= slot_half_height:\n",
    "                direction = mydesign[i].y/abs(mydesign[i].y)  # 1 or -1\n",
    "                mydesign[i].y = direction * \\\n",
    "                    (slot_half_height-(EW-overlap))  # make slot\n",
    "                # at curvature to end of slots and make sure they protrude by amp_in_out regardless of the shape/frequency of the inner wave\n",
    "                mydesign[i].y -= direction*amp_in_out * \\\n",
    "                    (1-cos((tau/4)*mydesign[i].x/(slot_half_width+EW)))**5\n",
    "            elif abs(mydesign[i].x) <= slot_half_width+EW and abs(mydesign[i].y) <= slot_half_height:\n",
    "                direction = mydesign[i].y/abs(mydesign[i].y)  # 1 or -1\n",
    "                # make long side to slot (in case slot end is in a shallow bit of the inner wave)\n",
    "                mydesign[i].y = direction * \\\n",
    "                    (slot_half_height-(EW-overlap)-amp_in_out)\n",
    "\n",
    "# vase mode up for the last half period\n",
    "ramp_points = int(segs/(2*(angles-1)))\n",
    "for i in range(ramp_points):\n",
    "  mydesign[-ramp_points +\n",
    "           i] = fc.move(mydesign[-ramp_points+i], fc.Vector(z=EH*(i/ramp_points)))\n",
    "\n",
    "# layers = 1\n",
    "mydesign_multilayer = fc.flatten(\n",
    "    [fc.move(mydesign, fc.Vector(z=EH*i)) for i in range(layers)])\n",
    "\n",
    "# narrow the walls towards the middle of the Z height\n",
    "for i in range(len(mydesign_multilayer)):\n",
    "  z_fraction = mydesign_multilayer[i].z / height\n",
    "  radius_now = fc.point_to_polar(mydesign_multilayer[i], centre).radius\n",
    "  radius_offset = mid_contraction_fraction * \\\n",
    "      (medial_rad - radius_now) * (0.5-0.5*cos(z_fraction*tau))**0.5\n",
    "  mydesign_multilayer[i] = fc.move_polar(\n",
    "      mydesign_multilayer[i], centre, radius_offset, 0)\n",
    "\n",
    "# squash the design a bit so the phone expands it slightly and is therefore gripped better - proportionally reduce radius and increase arc length\n",
    "mydesign_squashed = []\n",
    "for i in range(len(mydesign_multilayer)):\n",
    "    polar_now = fc.point_to_polar(mydesign_multilayer[i], centre)\n",
    "    # make sure it is 0 to tau, not -pi to +pi\n",
    "    angle_now = (polar_now.angle+tau) % tau\n",
    "    angle_shift = (angle_now - tau/2)*all_contraction_fraction\n",
    "    rad_shift = -polar_now.radius*all_contraction_fraction\n",
    "    mydesign_squashed.append(fc.move_polar(\n",
    "        mydesign_multilayer[i], centre, deepcopy(rad_shift), deepcopy(angle_shift)))\n",
    "\n",
    "mydesign_multilayer = mydesign_squashed\n",
    "\n",
    "# rotate design into a nicer print orientation\n",
    "mydesign_multilayer = fc.move_polar(mydesign_multilayer, centre, 0, -3*tau/8)\n",
    "\n",
    "# add speed control\n",
    "speed_increments = 100\n",
    "for i in range(speed_increments+1):\n",
    "  speed_fraction = i/speed_increments\n",
    "  mydesign_multilayer.insert(int(points_per_layer+speed_fraction*(layers_for_speed_increase*points_per_layer)),\n",
    "                             fc.Printer(print_speed=initial_print_speed + speed_fraction*(main_print_speed-initial_print_speed)))\n",
    "\n",
    "# generate gcode\n",
    "path_offset_xy = medial_rad + amp_in_out + amp_mid + 20\n",
    "\n",
    "\n",
    "# offset the whole procedure. z dictates the gap between the nozzle and the bed for the first layer, assuming the model was designed with a first layer z-position of 0\n",
    "model_offset = fc.Vector(x=path_offset_xy, y=path_offset_xy, z=0.8*EH)\n",
    "\n",
    "steps = fc.move(mydesign_multilayer, model_offset)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "# preview the design or save to gcode\n",
    "\n",
    "if target == 'gcode':\n",
    "    # add point at start of print for a nice lead-in line\n",
    "    steps.insert(0, fc.Point(x=path_offset_xy-medial_rad, y=path_offset_xy-medial_rad, z=steps[0].z))\n",
    "    gcode_controls = fc.GcodeControls(\n",
    "        printer_name=printer_name,\n",
    "    \n",
    "        initialization_data={\n",
    "            'primer': 'front_lines_then_y',\n",
    "            'print_speed': initial_print_speed,\n",
    "            'nozzle_temp': nozzle_temp,\n",
    "            'bed_temp': bed_temp,\n",
    "            'fan_percent': fan_percent,\n",
    "            'extrusion_width': EW,\n",
    "            'extrusion_height': EH})\n",
    "    gcode = fc.transform(steps, 'gcode', gcode_controls)\n    open(f'{design_name}.gcode', 'w').write(gcode)\n    files.download(f'{design_name}.gcode')\n",
    "else:\n",
    "    # add annotations and plot\n",
    "    steps.append(fc.PlotAnnotation(point=fc.Point(x=path_offset_xy, y=path_offset_xy, z=0), label='Zero-travel printpath'))\n",
    "    steps.append(fc.PlotAnnotation(point=fc.Point(x=0.8*path_offset_xy, y=0.8*path_offset_xy, z=0), label='Not all layers shown in this preview'))\n",
    "    steps.append(fc.PlotAnnotation(point=fc.Point(x=0.6*path_offset_xy, y=0.6*path_offset_xy, z=0), label=\"Print as fast as you can melt polymer - try speed=200% at +20'C\"))\n",
    "    if angry: \n",
    "        steps.append(fc.PlotAnnotation(point=fc.Point(x=0.4*path_offset_xy, y=0.4*path_offset_xy, z=0), label=\"Phone slots not added in angry mode - set Stand Angles = 11 to 15 for a snug fit between final two angry spikes\"))\n",
    "    elif angles > 13:\n",
    "        steps.append(fc.PlotAnnotation(point=fc.Point(x=0.4*path_offset_xy, y=0.4*path_offset_xy, z=0), label='Phone slots not added in angry mode - set Stand Angles = 11 to 15 for a snug fit between final two angry spikes'))\n",
    "    fc.transform(steps, 'plot', fc.PlotControls(style='line'))"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### please tell us what you're doing with FullControl!\n",
    "\n",
    "- tag FullControlXYZ on social media ([twitter](https://twitter.com/FullControlXYZ), [instagram](https://www.instagram.com/fullcontrolxyz/), [linkedin](https://www.linkedin.com/in/andrew-gleadall-068587119/), [tiktok](https://www.tiktok.com/@fullcontrolxyz))\n",
    "- email [info@fullcontrol.xyz](mailto:info@fullcontrol.xyz)\n",
    "- post on the [subreddit](https://reddit.com/r/fullcontrol)\n",
    "- post in the [github discussions or issues tabs](https://github.com/FullControlXYZ/fullcontrol/issues)\n",
    "\n",
    "in publications, please cite the original FullControl paper and the github repo for the new python version:\n",
    "\n",
    "- Gleadall, A. (2021). FullControl GCode Designer: open-source software for unconstrained design in additive manufacturing. Additive Manufacturing, 46, 102109. \n",
    "- Gleadall, A. and Leas, D. (2023). FullControl [electronic resource: python source code]. available at: https://github.com/FullControlXYZ/fullcontrol"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.3"
  },
  "vscode": {
   "interpreter": {
    "hash": "2b13a99eb0d91dd901c683fa32c6210ac0c6779bab056ce7c570b3b366dfe237"
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
